﻿namespace FxCopContrib
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Text.RegularExpressions;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// This rule tries to find values that are being converted by parsing the string representation of the original value,
    /// usually a simple cast or a call to Convert.ToType suffices here.
    /// </summary>
    /// <remarks>
    ///     This rule flags any string value that is assigned with a variant of ToString (either <c>object.ToString</c>, <c>string.Empty+value</c> or <c>""+value</c>) which
    ///     is subsequently passed to Parse, Convert.ToType or TryParse.
    /// </remarks>
    /// <example>
    /// <code>
    ///   string tmp = dataReader.GetValue(1).ToString();
    ///   decimal value = decimal.Parse(tmp);
    /// </code>
    /// Probably should be:
    /// <code>
    ///   decimal value = (decimal)dataReader.GetValue(1);
    /// </code>
    /// Or:
    /// <code>
    ///   decimal value = Convert.ToDecimal(dataReader.GetValue(1));
    /// </code>
    /// </example>
    public class DoNotUseParseAndToStringToConvertTypes : RuleBase
    {
        /// <summary>
        /// Standard FxCop Rule constructor.
        /// </summary>
        public DoNotUseParseAndToStringToConvertTypes()
            : base("DoNotUseParseAndToStringToConvertTypes") { }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="member"></param>
        /// <returns></returns>
        public override ProblemCollection Check(Member member)
        {
            variablesHoldingToString = new List<Node>();
            Visit(member);
            return this.Problems;
        }

        private List<Node> variablesHoldingToString;
        private List<Local> unassignedLocals;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="method"></param>
        public override void VisitMethod(Method method)
        {

            unassignedLocals = new List<Local>();
            if (method.Locals != null)
            {
                unassignedLocals.AddRange(method.Locals);
            }
            base.VisitMethod(method);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="assignment"></param>
        public override void VisitAssignmentStatement(AssignmentStatement assignment)
        {
            if (assignment.Target is Local)
            {
                unassignedLocals.Remove((Local)assignment.Target);
            }

            MethodCall assignmentSource = assignment.Source as MethodCall;
            if (assignmentSource != null)
            {
                if (IsCallToToString(assignmentSource))
                {
                    variablesHoldingToString.Add(assignment.Target);
                }
            }
            base.VisitAssignmentStatement(assignment);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength", Justification="Specifically looking for string.Empty.")]
        private static bool IsCallToToString(MethodCall methodCall)
        {
            MemberBinding binding = methodCall.Callee as MemberBinding;
            if (binding != null)
            {
                // string x = 32.ToString()
                if (binding.BoundMember.Name.Name == "ToString")
                {
                    return true;
                }
                else if (binding.BoundMember.DeclaringType != null
                    && FrameworkTypes.String.GetMembersNamed(Identifier.For("Concat")).Contains(binding.BoundMember))
                {
                    // string x = "" + int
                    // string x = "" + object
                    if (methodCall.Operands.Count == 1 &&
                        methodCall.Operands[0].Type != FrameworkTypes.String)
                    {
                        return true;
                    }

                    // string x = "" + int depending on compiler settings.
                    else if (methodCall.Operands.Count == 2
                        && methodCall.Operands[0] is Literal
                        && string.Equals(((Literal)methodCall.Operands[0]).Value as string, string.Empty, System.StringComparison.OrdinalIgnoreCase)
                        && methodCall.Operands[1].Type != FrameworkTypes.String
                        )
                    {
                        return true;
                    }

                    // string x = string.Empty + int
                    else if (methodCall.Operands.Count == 2
                        && methodCall.Operands[0] is MemberBinding
                        && FrameworkTypes.String.GetMembersNamed(Identifier.For("Empty")).Contains(((MemberBinding)methodCall.Operands[0]).BoundMember)
                        && methodCall.Operands[1].Type != FrameworkTypes.String
                        )
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        internal static readonly Regex IsConversionMethodExpression = new Regex("^To(?!String)[A-Z]", RegexOptions.Compiled);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="call"></param>
        public override void VisitMethodCall(MethodCall call)
        {
            MemberBinding binding = call.Callee as MemberBinding;
            if (IsParseOrConversionMethod(binding))
            {
                // if argument is a Node containing ToString value
                if (call.Operands.Intersect(variablesHoldingToString).Count() > 0)
                {
                    this.Problems.Add(new Problem(base.GetResolution(), call));
                }
                else if (call.Operands.Count >= 1 &&
                    (call.Operands[0].NodeType == NodeType.Call || call.Operands[0].NodeType == NodeType.Callvirt))
                {
                    if (IsCallToToString((MethodCall)call.Operands[0]))
                    {
                        this.Problems.Add(new Problem(base.GetResolution(), call));
                    }
                }
            }
            base.VisitMethodCall(call);
        }

        private static bool IsParseOrConversionMethod(MemberBinding binding)
        {
            return (binding != null) &&
                (binding.BoundMember.Name.Name == "Parse"
                || binding.BoundMember.Name.Name == "TryParse"
                || (binding.BoundMember.DeclaringType != null && binding.BoundMember.DeclaringType.Name.Name == "Convert"))
                || IsConversionMethodExpression.IsMatch(binding.BoundMember.Name.Name);
        }
    }
}
